#!/usr/bin/php
<?php

// Requirements
require_once('Console/CommandLine.php');
require_once('Console/Color.php');
require_once('Services/W3C/HTMLValidator.php');


$cl_parser = new Console_CommandLine(array(
	'description' => 'Batch HTML Validator',
	'version' => '0.0.1'
));

$cl_parser->addArgument(
	'path',
	array(
		'optional'     => false,
		'description'  => 'Path of the file containing a list of URLs, or a URL',
		'help_name'    => 'Path/URL'
	)
);

$cl_parser->addOption(
	'recurse',
	array(
		'short_name'   => '-r',
		'long_name'    => '--recurse',
		'description'  => 'Type of recursion if any.  Valid options are: none, domain, subpath (default), regex',
		'choices'      => array('none','domain','subpath','regex'),
		'action'       => 'StoreString'
	)
);
$cl_parser->addOption(
	'validator',
	array(
		'short_name'   => '-v',
		'long_name'    => '--validator',
		'description'  => 'Validator to use. Default is validator.w3.org',
		'action'       => 'StoreString'
	)
);
$cl_parser->addOption(
	'log_file',
	array(
		'short_name'   => '-l',
		'long_name'    => '--log-file',
		'description'  => 'Log to specified file',
		'action'       => 'StoreString'
	)
);
$cl_parser->addOption(
	'log_file_append',
	array(
		'short_name'   => '-L',
		'long_name'    => '--log-file-append',
		'description'  => 'Append log to file specified by -l',
		'action'       => 'StoreTrue'
	)
);
$cl_parser->addOption(
	'quiet',
	array(
		'short_name'   => '-q',
		'long_name'    => '--quiet',
		'description'  => 'If supplied STDOUT will not be used',
		'action'       => 'StoreTrue'
	)
);
$cl_parser->addOption(
	'sleep',
	array(
		'short_name'   => '-s',
		'long_name'    => '--sleep',
		'description'  => 'Number of seconds to sleep between requests',
		'action'       => 'StoreInt'
	)
);
$cl_parser->addOption(
	'proxy_host',
	array(
		'long_name'    => '--proxy-host',
		'description'  => 'Proxy HOST to use for fetching URLs',
		'action'       => 'StoreString'
	)
);
$cl_parser->addOption(
	'proxy_port',
	array(
		'long_name'    => '--proxy-port',
		'description'  => 'Proxy PORT to use for fetching URLs',
		'action'       => 'StoreString'
	)
);
$cl_parser->addOption(
	'validator_proxy_host',
	array(
		'long_name'    => '--validator-proxy-host',
		'description'  => 'Proxy HOST to use for the validator',
		'action'       => 'StoreString'
	)
);
$cl_parser->addOption(
	'validator_proxy_port',
	array(
		'long_name'    => '--validator-proxy-port',
		'description'  => 'Proxy PORT to use for the validator',
		'action'       => 'StoreInt'
	)
);
$cl_parser->addOption(
	'mode',
	array(
		'short_name'   => '-m',
		'long_name'    => '--mode',
		'description'  => 'Valid options are: url (default), upload',
		'choices'      => array('url','upload'),
		'action'       => 'StoreString'
	)
);


try {
	$cl_result = $cl_parser->parse();
	$arguments = $cl_result->args;
	$options = $cl_result->options;
} catch (Exception $exc) {
   $cl_parser->displayError($exc->getMessage());
}

$bv = new BatchValidator($arguments['path'],$options);
$bv->start_validation();




class BatchValidator {
	
	
	var $urls_to_parse = array();
	
	var $base_url;
	var $url_list = array();
	var $url_index = 0;
	var $url_results = array();
	
	var $path_argument;
	
	var $validator;
	var $http_request;
	
	var $log_file;
	
	var $options = array();
	var $defaults = array(
		'recurse' => 'subpath',
		'validator' => 'http://validator.w3.org/check',
		'silent' => false,
		'log_file' => false,
		'log_file_append' => false,
		'mode' => 'url',
		'proxy_host' => 'www-cache.reith.bbc.co.uk',
		'proxy_port' => 80,
		'validator_proxy_host' => null,
		'validator_proxy_port' => null,
		'sleep' => 1
	);
	
	function __construct($path_argument,$options) {
		//print_r($options);
		$this->path_argument = $path_argument;
		//$this->options = array_merge($this->defaults,$options); // latter overwrites former
		foreach ( $this->defaults as $k=>$v ) {
			$this->options[$k] = (empty($options[$k])) ? $v : $options[$k];
		}
		
		// setup http request
		$this->http_request = new HTTP_Request2(
		    null,
		    null,
		    array(
		        'proxy_host'=>$this->options['proxy_host'],
		        'proxy_port'=>$this->options['proxy_port']
		    )
		);
		
		// setup validator
		$this->validator = new Services_W3C_HTMLValidator();
		$this->validator->validator_uri = 'http://localhost/w3c-validator/check';
		if ( $this->options['validator_proxy_port'] != null ) {
			$v_request = new HTTP_Request2(
			    null,
			    null,
			    array(
			        'proxy_host'=>$this->options['validator_proxy_host'],
			        'proxy_port'=>$this->options['validator_proxy_port']
			    )
			);
			$this->validator->setRequest($v_request);
		}
	}
	
	function start_validation() {
		$this->_log("START");
		$this->_flog_open();
		$this->_flog_start();
		//$this->validator->validator_uri =  $this->options['validator'];
		if ( $this->is_absolute_url($this->path_argument) ) {
			$this->url_list = array($this->path_argument);
			$this->url_index = 0;
			while ( isset($this->url_list[$this->url_index]) ) {
				$current_url = $this->url_list[$this->url_index];
				$result = $this->validate_page($current_url);
				$this->url_results[$this->url_index] = $result;
				$this->_parse_result($result,$current_url);
				$this->url_index++;
				sleep($this->options['sleep']);
			}
			$this->end_validation();
		} else {
			$this->_log("VALIDATE BY URL LIST NOT IMPLEMENTED YET");
		}
	}
	
	
	function end_validation() {
		$num_ok = $num_err = 0;
		$num_ttl = count($this->url_results);
		for ($i=0, $imax=count($this->url_list); $i<$imax; $i++ ) {
			$url = $this->url_list[$i];
			$result = $this->url_results[$i];
			$this->_flog_result($url,$result);
			if ( $result->validity ) {
				$num_ok++;
			} else {
				$num_err++;
			}
		}
		$this->_log("END");
		$percentage_error = round(100 * $num_err / $num_ttl);
		$this->_log("$num_err pages out of $num_ttl had errors ($percentage_error%)");
	}
	
	/**
	* validates a url
	* 
	* @return Validator_Service_Result
	*/
	function validate_page($url) {
		//$this->_log($this->options['mode']);
		if ( $this->options['mode'] == 'url' ) {
			try {
				$this->http_request->setUrl($url);
				$response = $this->http_request->send();
			} catch (Exception $e) {
				$this->_error("Failed to fetch page for validation");
				return false;
			}
			if ( $response->getStatus() == 301 || $response->getStatus() == 302) {
				return $this->validate_page($response->getHeader('location'));
			}
			try {
				$result = $this->validator->validateFragment($response->getBody());
				$this->extract_urls_from_html($response->getBody(),$url);
			} catch (Exception $e) {
				$this->_log("Failed to validate page");
				return false;
			}
			return $result;
		} else {
			$this->_log("VALIDATE BY UPLOAD NOT IMPLEMENTED YET");
		}
		
	}
	
	function url_should_be_validated($url,$current_url) {
		//$this->_log("---");
		if ( !$this->is_absolute_url($url) ) {
			$url = $this->make_absolute_url($url,$current_url);
		}
		// @TODO: YIKES!
		//$this->_log("checking $url againt {$this->path_argument}");
		//$this->_log((strpos($url,$this->path_argument)===false));
		if ( strpos($url,$this->path_argument) === 0 ) return true;
	}
	
	function make_absolute_url($url,$current_url) {
		if ( $this->is_absolute_url($url) ) return $url;
		$base_domain = parse_url($current_url,PHP_URL_SCHEME) . "://" . parse_url($current_url,PHP_URL_HOST) ;
		if ( $url[0] == "/" ) {
			//$this->_log("slash at start");
			return $base_domain . $url;
		}
		$last_slash = strrpos($url,"/") + 1;
		$url_path = substr($url,0,$last_slash);
		return $base_domain . $url_path . $url;
	}
	
	function is_absolute_url($url) {
		$firstbit = substr($url,0,4);
		$is_absolute = ($firstbit == "http");
		//if ($is_absolute) {$this->_log("$url IS ABSOLUTE"); } else { $this->_log("$url IS NOT ABSOLUTE"); }
		return $is_absolute;
	}
	
	
	function extract_urls_from_html($html,$current_url) {
		preg_match_all('/<a[^>]* href="([^"]+)"/',$html,$matches);
		foreach ( $matches[1] as $url ) {
			//$this->_log("new extracted url : ".$url . " and current url is: $current_url");
			$url = $this->make_absolute_url($url,$current_url);
			if ( strpos($url,"#") ) {
				$url = substr($url,0,strpos($url,'#'));
			}
			if ( in_array($url,$this->url_list) ) continue;
			if ( $this->url_should_be_validated($url,$current_url) ) {
				$this->url_list[] = $url;
				//$this->_log("Appending $url to list, which is now " . count($this->url_list) . " items long");
			}
		}
	}
	
	
	private function _parse_result($result,$url) {
		$num = $this->url_index + 1;
		$max = count($this->url_list);
		$outof = "$num of $max";
		if ( !$result->validity ) {
			$this->_error("[".count($result->errors)."] ".$url." $outof");
		} else {
			$this->_pass($url." $outof");
		}
		return $result;
	}
		
	
	private function _log($str,$lvl=0) {
		$output = "";
		if ( $lvl ) $output .= str_repeat(".",$lvl);
		$output .= "$str";
		$output .= "\n";
		print $output;
	}
	
	private function _warn($str,$lvl=0) {
		$this->_log(Console_Color::convert("%r$str%n"),$lvl);
	}
	private function _error($str,$lvl=0) {
		$this->_log(Console_Color::convert("%y$str%n"),$lvl);
	}
	private function _pass($str,$lvl=0) {
		$this->_log(Console_Color::convert("%g$str%n"),$lvl);
	}
	
	private function _reconstruct_command() {
		global $argv;
		$c = '';
		foreach ($argv as $a) $c .= "$a ";
		return trim($c);
	}
	
	
	
	
	
	private function _flog_result($url,$result) {
	    if ( !$this->options['log_file'] ) return;
		$class = ($result->validity) ? "pass" : "fail";
		$this->_flog("<div class=\"url $class\">");
		$this->_flog("<h2>$url</h2>");
		if ( $result->validity ) {
			$this->_flog("<p class=\"result\">PASSED VALIDATION</p>");
		} else {
		    $error_count = count($result->errors);
		    $this->_flog(<<<TABLE
<p class="result">
    <a href="#" onclick="toggleResultTable({$i}); return false">
        FAILED VALIDATION WITH {$error_count} ERRORS
    </a>
</p>
<table id="result{$i}" cellspacing="0" cellpadding="0" class="errors" width="100%" style="display:none">
    <thead><tr><th>Line</th><th>Col</th><th>Error</th></tr></thead>
    <tbody>
TABLE
);
			foreach ( $result->errors as $error ) {
				$this->_flog("<tr>");
				$this->_flog("<td>{$error->line}</td>");
				$this->_flog("<td>{$error->col}</td>");
				$this->_flog("<td>{$error->message}</td>");
				$this->_flog("</tr>");
			}
			$this->_flog("</tbody></table>");
		}
		$this->_flog("</div>");
	}
	private function _flog_open() {
	    if ( !$this->options['log_file'] ) return;
		$mode = ($this->options['log_file_append']) ? 'a' : 'w';
		$this->log_file = fopen($this->options['log_file'],$mode);
		return $this->log_file;
	}
	private function _flog_start() {
	    if ( !$this->options['log_file'] ) return;
		$logdate = date("Y-m-d H:i:s");
		$logtitle = "Validtion result ".$logdate." ".$this->_reconstruct_command();
	    if ( !$this->options['log_file_append'] ) {
			$this->_flog(<<<HEAD
<html>
<head>
	<title>{$logtitle}</title>
	<style type="text/css">
		body { font-family:Arial,Helvetica,Verdana,sans-serif; font-size:80%; }
		.url { padding:10px; }
		.url h2 { margin:0px; }
		.url table { display:none; width:100%; border-top:1px solid #000; border-left:1px solid #000; margin-top:5px; }
		.url table th { background-color:#000; color:#fff; }
		.url table td { border-bottom:1px solid #000; border-right:1px solid #000; padding:2px; font-size:80%; }
		.pass { background-color:#DBFFCF; }
		.fail { background-color:#FFCFCF; }
		p.result { margin:0px;}
	</style>
	<script type="text/javascript">
		function toggleResultTable(i) {
			var table = document.getElementById('result'+i);
			if ( table.style.display == 'none' ) {
				table.style.display = 'inline-table';
			} else {
				table.style.display = 'none';
			}
		}
	</script>
</head>
<body>
HEAD
);
        }
        $this->_flog("<h1>{$logtitle}</h1>");
	}
	private function _flog($str) {
	    if ( !$this->options['log_file'] ) return;
		fwrite($this->log_file,$str."\r\n");
	}
	private function _flog_close() {
	    if ( !$this->options['log_file'] ) return;
	    fclose($this->log_file);
	}
	
}






?>